Skip to content

fix(skills,stack,scripts): shipped decrypt guidance, and a sound package-path linter (#772 review findings 14, 15) - #789

Merged
tobyhede merged 5 commits into
remove-v2from
fix/shipped-docs-and-lint-gates
Jul 27, 2026
Merged

fix(skills,stack,scripts): shipped decrypt guidance, and a sound package-path linter (#772 review findings 14, 15)#789
tobyhede merged 5 commits into
remove-v2from
fix/shipped-docs-and-lint-gates

Conversation

@tobyhede

Copy link
Copy Markdown
Contributor

Review remediation for #772 — findings 14 and 15, plus the contradictory-changeset item from the cut-for-length list.

Finding 14 — the shipped docs contradict the code

decryptModel / bulkDecryptModels return an AuditableDecryptModelOperation: thenable, carrying .withLockContext() and .audit(). Verified both ways — seven chaining forms compile, and a credential-free runtime test (protect-ffi mocked) passes 7/7.

Three sites in skills/stash-encryption/SKILL.md and four in packages/stack/README.md said the opposite ("a plain Promise<Result<...>>", "no .withLockContext() to chain"), steering agents away from the exact .audit() chain the audit-on-decrypt changeset advertises. The skill contradicted its own reference table, which was already correct: 8b744309 fixed that table and nothing else, despite a commit message claiming it had updated the guidance.

Both ship — the skill into customer repos via installSkills(), the README in the @cipherstash/stack tarball.

SKILL.md:483 says the same thing about the WASM entry and is correct — that client really does return a bare promise with no lock-context argument. Deliberately untouched.

Also: protectOps.eq in the setup prompt stash init writes for coding agents. One occurrence repo-wide, and no such export exists anywhere; the real API is createEncryptionOperators(client), conventionally ops.

Finding 15 — the linter had a false positive and a false negative

  • False positive: the name capture had no right anchor, so a sentence-final packages/stack. swallowed the period and reported a live package as dead. Uppercase was excluded from the class too, so packages/Foo was never checked at all.
  • False negative: livePackages came from readdirSync — the working tree. Deleting a package leaves dist/ and node_modules/ behind, so every reference to it kept passing. That is precisely what this linter was commissioned to catch, silently unenforced on any checkout that had previously built the package. Now derived from git ls-files. Deliberately not "has a package.json" — packages/utils has none and is live.
  • scripts/ was never scanned, so the linters never checked themselves. Adding it immediately surfaced a dead packages/drizzle allowlist entry in lint-no-hardcoded-runners.mjs for a path that never existed in git history. Self-test fixtures stay exempt; they must name dead packages.

Severity, for the record: CI was never affected — actions/checkout gives a fresh workspace with caching disabled — and main carries no required status checks. This was a local-developer papercut plus a real soundness hole, not the broken build the review implies.

Verified the false negative is closed by re-creating a stale untracked packages/protect/dist shell: the reference is now flagged where it previously passed.

Cut-list — contradictory changesets in one release

.changeset/dynamodb-eql-v3.md claimed "EQL v2 tables continue to work unchanged" and "no existing caller needs to change", while stack-dynamodb-v2-write-removal.md announced the v2 encrypt overloads as removed — both unreleased, so both render into the same CHANGELOG. The code sides with removal (dynamodb/types.ts declares Table extends AnyV3Table with no v2 overload). The claims are now scoped to the decrypt path, which genuinely is unchanged.

Verification

pnpm run test:scripts → 93 passed (90 + 3 new fixture-driven cases). lint:package-paths and lint:runners both clean. Biome: 0 errors.

@tobyhede
tobyhede requested a review from a team as a code owner July 25, 2026 04:44
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0585f4d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Minor
@cipherstash/stack Minor
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/bench Patch
@cipherstash/prisma-next Minor
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch
@cipherstash/wizard Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f7bcad35-0839-48fb-8840-7b02732ae5f5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shipped-docs-and-lint-gates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve.

All three parts check out. Confirmed the typed client genuinely returns a chainable AuditableDecryptModelOperation with .audit()/.withLockContext(), so the corrected decrypt-chaining docs now match the code (the old "plain Promise" wording was the bug), and the WASM-entry statement was correctly left intact. The package-path linter's readdirSync → git ls-files change is the right soundness fix, and protectOps no longer appears in any shipped surface. Changeset targeting is correct (stash patch + @cipherstash/stack patch), and the PR also repairs a real contradiction between two unreleased changesets.

Non-blocking:

  • The highest-value fix — livePackages from git ls-files — has no dedicated regression test. It's awkward to test (needs a tracked-vs-untracked dir fixture) but achievable; without it a future revert to readdirSync would leave every test green.
  • setup-prompt.ts:283 now points at createEncryptionOperators, but this prompt sets up the v3 flow, where the Drizzle factory is createEncryptionOperatorsV3. Not wrong the way protectOps was (the v2 name is a real export), just off-version — consider createEncryptionOperatorsV3 or keeping it generic.

Comment thread skills/stash-encryption/SKILL.md

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the decrypt-guidance corrections and the package-path linter, verifying each claim against source.

Approving. This is a precise, well-scoped remediation.

Verified

  • The corrected decrypt-chaining docs (skill + packages/stack/README.md) now match the real typed-client API: decryptModel/bulkDecryptModels return AuditableDecryptModelOperation (thenable, carrying both .audit() and .withLockContext()), the optional positional lockContext arg exists, and the "double-bind throws" behaviour is real (mapped-decrypt.ts:86-90).
  • protectOps.eqops.eq from createEncryptionOperators(client) is correct on this remove-v2 base (the V3 suffix was collapsed to the package root).
  • The linter fix is genuinely sound: the regex right-anchor kills the sentence-final false positive, uppercase is now checked, and the readdirSync → git ls-files switch closes the real false-negative (reproduced end-to-end — an untracked packages/*/dist shell is now flagged).
  • pnpm test:scripts → 93 passed; Biome clean on changed files. Changeset targeting (stash patch + @cipherstash/stack patch) is correct per AGENTS.md.

Non-blocking nits

  • The highest-value linter fix (tracked-vs-untracked via git ls-files) has no dedicated regression test, so a future revert to readdirSync would leave the suite green. It's testable (untracked packages/<name>/dist shell). Matches freshtonic's note.
  • setup-prompt.ts:283createEncryptionOperators(client) is the Drizzle-specific factory but the prompt also covers Supabase (where operators come via the encryptedSupabase wrapper). It hedges with "see the integration skill", so acceptable; consider phrasing it as Drizzle-specific.
  • Cosmetic: offender-path rendering emits ../../.. for a target outside the repo root (unreachable via the default target list).

Note: freshtonic's suggestion to use createEncryptionOperatorsV3 is stale on this base — that name was de-suffixed; the PR's choice is correct.

tobyhede added a commit that referenced this pull request Jul 27, 2026
… repeats

Remediation for the two review rounds on #789, plus what verifying them turned
up in the same code.

**The linter fix the review asked for twice.** `livePackages` moving from
`readdirSync` to `git ls-files` was the highest-value change in the PR and had
no test that pinned it. It still doesn't fail under the revert anyone would
actually write: keeping the git call and unioning the filesystem back in passes
every existing test while fully restoring the false negative. The new fixture
builds the discriminating case — a package deleted from git whose gitignored
`dist/` and `node_modules/` shells survive on disk — and is the only test that
fails under both a naive `readdirSync` revert and that hybrid. The
`git status` guard is scoped to the probe, not to `packages/`, so an unrelated
uncommitted file doesn't turn it into an assertion about nothing.

**A missing target no longer passes in silence.** A linter whose entire job is
catching dead paths in configuration skipped dead paths in its own: rename a
target and it dropped out of coverage forever, green. Its sibling already
exits 2 for a stale allowlist entry. Both now do, and both report a target
outside the repo by name instead of as a `../../../../..` chain climbing out
of the root — the review's cosmetic nit, which turned out to sit in both files.

**Every step that names an integration-specific API now branches.** Step 5 was
fixed for this; steps 1, 2 and the read-path step were not. A Prisma Next
project was sent at `types.*` / `encryptedTable` — the client `stash schema
build` explicitly refuses to scaffold for it — and a plain-Postgres project was
pointed three times at "the integration skill" it never gets installed.
`encryptQuery` is shown taking the schema objects rather than an
object-shorthand that read as three required strings. `queryOperatorGuidance`
is a switch with a neutral default rather than an if-chain ending in Drizzle's
answer: tsup transpiles without type-checking and this package has no
typecheck script, so nothing would have caught a fifth integration inheriting
it.

One test asserted nothing: it scoped to a `#### Encryption cutover` heading
that does not exist, and `substring(-1)` returns the whole document.
tobyhede added 5 commits July 27, 2026 19:09
… the package-path linter sound

Finding 14 — decryptModel/bulkDecryptModels return an
AuditableDecryptModelOperation: thenable, with .withLockContext() and .audit().
Three sites in skills/stash-encryption and four in packages/stack/README.md
said the opposite ("a plain Promise<Result<...>>", "no .withLockContext() to
chain"), steering agents away from the very .audit() chain the audit-on-decrypt
changeset advertises. The skill contradicted its own reference table, which was
already right — 8b74430 fixed that table and nothing else, despite a commit
message claiming otherwise.

Both files ship: the skill inside the stash tarball and thence into customer
repos via installSkills(), the README inside the @cipherstash/stack tarball.
The equivalent statement about the WASM entry is CORRECT and deliberately
untouched — that client really does return a bare promise.

Also `protectOps.eq` in the setup prompt stash init writes for coding agents.
One occurrence repo-wide, and no such export exists; the real API is
createEncryptionOperators(client), conventionally `ops`.

Finding 15 — the package-path linter had a false positive and a false negative,
both fixed with the fixture-driven self-tests the suite already uses:

- The name capture had no right anchor, so a sentence-final `packages/stack.`
  swallowed the period and reported a LIVE package as dead. Uppercase was also
  excluded from the class, so a capitalised name was never checked at all.
- livePackages came from readdirSync, i.e. the working tree. Deleting a package
  leaves dist/ and node_modules/ behind, so every reference to it kept passing —
  the exact case the linter was commissioned to catch, silently unenforced on
  any checkout that had built the package. Now derived from `git ls-files`.
  Deliberately not "has a package.json": packages/utils has none and is live.
- scripts/ was not scanned, so the linters never checked themselves. Adding it
  immediately surfaced a dead allowlist entry in lint-no-hardcoded-runners for
  a path that never existed in git history. Self-test fixtures stay exempt —
  they must name dead packages.

Severity note for the record: CI was never affected (fresh checkout, caching
disabled), and main carries no required status checks — this was a local
developer papercut plus a real soundness hole, not a broken build.

Finally, resolves two changesets that contradicted each other in the same
release: dynamodb-eql-v3 claimed "EQL v2 tables continue to work unchanged" and
"no existing caller needs to change" while stack-dynamodb-v2-write-removal
announced the v2 encrypt overloads as removed. The code sides with removal;
the claims are now scoped to the decrypt path.
…tch their own rot

Follow-up to the #772 review findings, addressing what the review of that
review turned up.

`stash init`'s setup prompt named `createEncryptionOperators` unconditionally.
That symbol is exported by `@cipherstash/stack-drizzle` alone, so a Supabase or
Prisma Next project was sent after a package that is not in its dependency
tree — the previous `protectOps.eq` was wrong for everyone, this was wrong for
three integrations out of four. Step 5 now branches through
`queryOperatorGuidance()`, following the `migrationCommands()` pattern already
in the file: `ops.eq` for Drizzle, the `encryptedSupabase` wrapper's own
filters for Supabase, the `eql*` column operators for Prisma Next, and
`client.encryptQuery(...)` for plain Postgres — which is also pointed at
`stash-encryption`, since it is the default integration and installs no
integration skill, making "see the integration skill" a dangling pointer.

The package-path linter reported an untracked-but-present package as "does not
exist" — finding 15's false alarm pointed the other way, at a directory sitting
right there on disk. The live set now unions `git ls-files --others
--exclude-standard`. `--directory` is deliberately not passed: it collapses an
all-ignored directory to one entry and would resurrect exactly the `dist/`-and-
`node_modules/` shells the linter exists to catch (verified both ways). git
failing now exits 2 with an actionable message instead of a raw ENOENT stack
trace and exit 1, which was indistinguishable from a genuine lint failure, and
an empty live set refuses to run rather than flagging every reference at once.

The `scans scripts/ but not its fixtures` test asserted the repo was clean —
byte-for-byte what the suite's first test already asserted, and passing whether
or not `scripts` was in TARGETS. It now plants an offender in the scanned
directory. Verified by mutation: it dies when `scripts` is dropped, as the
untracked-package test dies without the union and the git-failure test dies
when the exit code is flipped.

The runners linter now requires each allowlist entry to exist and to still
contain an unexcused `npx` literal. It immediately found a second stale entry
the sibling linter structurally cannot see, since it only matches the
`packages/<name>` shape: `setup-prompt.ts` has had no `npx` since its switch
moved to `utils.ts`. Removed.

Also corrects two claims: the test comment saying `packages/drizzle` never
existed in git history (it was added speculatively by c671560, became
load-bearing 31 minutes later with 9d259e6, and rotted when 413ca39 deleted
the package), and the `AnyEncryptedTable` doc comment still promising v3 is
"purely additive and no existing caller has to change" — true of decrypt, false
of encrypt since the v2 write overloads were removed.
… repeats

Remediation for the two review rounds on #789, plus what verifying them turned
up in the same code.

**The linter fix the review asked for twice.** `livePackages` moving from
`readdirSync` to `git ls-files` was the highest-value change in the PR and had
no test that pinned it. It still doesn't fail under the revert anyone would
actually write: keeping the git call and unioning the filesystem back in passes
every existing test while fully restoring the false negative. The new fixture
builds the discriminating case — a package deleted from git whose gitignored
`dist/` and `node_modules/` shells survive on disk — and is the only test that
fails under both a naive `readdirSync` revert and that hybrid. The
`git status` guard is scoped to the probe, not to `packages/`, so an unrelated
uncommitted file doesn't turn it into an assertion about nothing.

**A missing target no longer passes in silence.** A linter whose entire job is
catching dead paths in configuration skipped dead paths in its own: rename a
target and it dropped out of coverage forever, green. Its sibling already
exits 2 for a stale allowlist entry. Both now do, and both report a target
outside the repo by name instead of as a `../../../../..` chain climbing out
of the root — the review's cosmetic nit, which turned out to sit in both files.

**Every step that names an integration-specific API now branches.** Step 5 was
fixed for this; steps 1, 2 and the read-path step were not. A Prisma Next
project was sent at `types.*` / `encryptedTable` — the client `stash schema
build` explicitly refuses to scaffold for it — and a plain-Postgres project was
pointed three times at "the integration skill" it never gets installed.
`encryptQuery` is shown taking the schema objects rather than an
object-shorthand that read as three required strings. `queryOperatorGuidance`
is a switch with a neutral default rather than an if-chain ending in Drizzle's
answer: tsup transpiles without type-checking and this package has no
typecheck script, so nothing would have caught a fifth integration inheriting
it.

One test asserted nothing: it scoped to a `#### Encryption cutover` heading
that does not exist, and `substring(-1)` returns the whole document.
…default

`stash plan` writes one of three templates. Two of them — the cutover plan and
the `--complete-rollout` plan — described the EQL v2 rename swap as the only
way to switch reads: "a single transaction renames `<col>` → `<col>_plaintext`
and `<col>_encrypted` → `<col>`", with the steps after it referring to
`<col>_plaintext` as though the rename had certainly happened.

On EQL v3, which is the default, none of that is true. `stash encrypt cutover`
refuses a v3 column outright — "Cut-over is not applicable to EQL v3 columns …
there is no rename step" (`encrypt/cutover.ts:119`) — and refuses entirely on a
v3-only database, where the `eql_v2_configuration` relation it needs does not
exist (`:142`). So on a default install the agent was being asked to draft a
plan around a command that will decline to run, and to name a
`<col>_plaintext` column that will never exist.

The implement prompt in this same file already splits the two correctly. This
copies that split into the templates that were missing it. The EQL version is
per-column — one database can hold both — so the templates tell the agent to
establish it per column (`stash encrypt status`) rather than deciding once for
the whole plan; a context-level flag would have been wrong for a mixed
database.

Also: the "this column is already encrypted" stop-and-ask keyed on the
`eql_v2_encrypted` udt alone. v3 columns carry `eql_v3_*` domains, so on the
default path the check could never fire. The agent reads the schema itself, so
naming both is sufficient here.

`introspect.ts:88` has the same v2-only assumption in code
(`isEqlEncrypted: row.udt_name === 'eql_v2_encrypted'`). Left alone
deliberately: it decides which columns `stash init` pre-selects as
already-managed, so widening it changes setup behaviour and wants its own
tests rather than riding along here.
`introspectDatabase` marked a column as CipherStash-managed only when its udt
was exactly `eql_v2_encrypted`:

    isEqlEncrypted: row.udt_name === 'eql_v2_encrypted'

v3 is the default generation and its columns carry per-domain types —
`eql_v3_text_search`, `eql_v3_integer_ord`, and so on — so on the default path
this was false for every encrypted column. The consequences are all in the
column picker: the table hint never said "N already encrypted", the
pre-selection notice never fired, and each encrypted column was displayed with
its plaintext `dataType` and left unticked. An encrypted column presented as
plaintext invites the user to encrypt it a second time, which is the direction
of wrongness that costs data rather than time.

`packages/wizard` already had the correct predicate, with the reasoning
written down (`isEqlEncryptedDomain`, `wizard-tools.ts:167`). This mirrors it
rather than inventing a second answer, and exports it so it is testable — the
old inline comparison sat inside a function that needs a live pg connection,
which is why nothing covered it.

Deliberately not `classifyEqlDomain` from `@cipherstash/migrate`, despite the
dependency already being present: that answers "which generation authors this"
and returns null for `eql_v2_encrypted`, since v2 is no longer authorable. The
question here is "is this encrypted at all", and v2 answers yes.

The per-column hint was also the hardcoded string `eql_v2_encrypted` for any
encrypted column, which mislabels a v3 column with a domain it does not have.
It now reports the column's actual udt, and the pre-selection notice lists the
domains it found instead of naming one it may not have seen.

Checked the rest of the CLI for the same assumption: every other site either
already handles both generations (`rewrite-migrations.ts` matches
`eql_v2_encrypted|eql_v3_[a-z0-9_]+`, `db-readers.ts`, `backfill.ts`) or is
legitimately v2-only (`cutover.ts`, the installer's CREATE TYPE permission
messages). Introspection was the sole outlier.
@tobyhede
tobyhede force-pushed the fix/shipped-docs-and-lint-gates branch from e8a1e5c to 0585f4d Compare July 27, 2026 09:12
@tobyhede

Copy link
Copy Markdown
Contributor Author

Audited the review feedback against the head commit (0585f4d2). All three non-blocking nits are closed — two of them by commits that landed after the approvals, which is why they weren't visible at review time.

1. git ls-files fix had no regression test — closed by 57f323ef.

scripts/__tests__/lint-no-dead-package-paths.test.mjs:135, "flags a deleted package whose gitignored dist/ shell survives on disk", builds exactly the fixture you described: packages/lint-dead-shell-probe/dist/index.js plus node_modules/.keep, no tracked files. It asserts exit 1 and that the offending path is named. The git status --porcelain guard at :146-151 is load-bearing — it proves the shell really is gitignored, so the --others --exclude-standard arm legitimately doesn't count it live.

Mutation-tested to confirm it discriminates:

  • revert livePackages to readdirSync → this test fails (expected +0 to be 1), plus the git-unavailable test as collateral
  • subtler hybrid — add --directory to the untracked ls-files call, which would collapse the all-ignored dir into one entry and resurrect the shell → this is the only test that fails, so it also pins the deliberate omission of --directory

The sibling test at :105 ("treats a package that exists but is not yet tracked as live") passes under both mutations, exactly as its comment claims — so the new test is doing real work, not shadowing an existing one.

2. Operator guidance scoped to Drizzle — closed by 69b754c9.

The hardcoded line you quoted at setup-prompt.ts:283 is gone; guidance is now a per-integration switch at setup-prompt.ts:125-139. Supabase gets the encryptedSupabase wrapper, Prisma Next gets the eql* column operators, plain Postgres gets client.encryptQuery(...), and the default: is deliberately neutral rather than falling through to Drizzle — packages/cli is tsup-transpiled without a typecheck, so a future fifth Integration variant would otherwise silently inherit Drizzle's string. Same treatment applied to schemaAuthoringGuidance (:98-109) and readPathGuidance (:148-157).

Pinned by five cases at __tests__/setup-prompt.test.ts:114-173, each asserting the positive symbol and the negative — Supabase, Prisma Next, and plain-Postgres prompts all assert not.toContain('createEncryptionOperators').

On the earlier createEncryptionOperatorsV3 suggestion: you've already flagged it as stale on this base, and that's right — the *V3 names were de-suffixed with no alias (.changeset/remove-eql-v2-drizzle-root.md:12). Worth noting there's a guard that would have caught it either way: scripts/__tests__/no-removed-drizzle-surface.test.mjs:18-19 lists createEncryptionOperatorsV3 as a forbidden string, and its SHIPPED_GLOBS covers packages/cli/src/commands/init/lib/*.ts — i.e. setup-prompt.ts itself.

3. ../../.. offender-path rendering — closed by 57f323ef, in both linters.

The nit was real. Reproduced at the reviewed commit (69b754c9): an out-of-repo target printed ../../../../far/README.md:1: .... At head both scripts fall back to the absolute path when relative() escapes the root — lint-no-dead-package-paths.mjs:173-188 and lint-no-hardcoded-runners.mjs:135-141, the latter keeping the repo-relative rel for the ALLOWLISTED_PATHS lookup so the allowlist still matches. Regression tests at lint-no-dead-package-paths.test.mjs:195 and lint-no-hardcoded-runners.test.mjs:56, both asserting output).not.toMatch(/\.\.\//).

Your parenthetical was right too — unreachable via defaults. Both default target lists are repo-relative with no .. and no absolute entry (lint-no-dead-package-paths.mjs:15-32, lint-no-hardcoded-runners.mjs:16-18), and walk() builds descendants with join(), so a default run can't produce a .. prefix. It's reachable only through the argv override. Fixed anyway, and the same defect turned out to sit in the sibling script that wasn't flagged.

Please re-check before merge. Head has moved since the approvals: 57f323ef, 24f66c610 and 0585f4d2 landed after, and the latter two are outside what the description covers — already-encrypted column detection on EQL v3 during init (introspect.ts, +71 lines of tests) and a plan-template fix that stopped a v2-only cutover being drafted against a v3 schema. I'll extend the PR description to cover them.

@tobyhede
tobyhede merged commit 7a9dfe5 into remove-v2 Jul 27, 2026
7 checks passed
@tobyhede
tobyhede deleted the fix/shipped-docs-and-lint-gates branch July 27, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants